#!/usr/bin/lua 

-- Author: Martin K. Schröder <mkschreder.uk@gmail.com>

require "ubus"
require "uloop"
local juci = require("juci/core"); 

function find(tbl, cb) 
	for _,v in pairs(tbl) do 
		if cb(v) then return v; end
	end
	return nil; 
end

uloop.init(); 

local conn = ubus.connect()
if not conn then
	error("Failed to connect to ubus")
end

local WifiMonitor = {
	scanresults = {}
}; 

function WifiMonitor:new(o)
	o = o or {}; 
	setmetatable(o, self); 
	self.__index = self; 
	return o; 
end 

function WifiMonitor:scan(radio)
	juci.shell("wlctl -i %s scan", (radio or "wl1")); 
end

function WifiMonitor:poll_scan_results(radio)
	local stdout = juci.shell("wlctl -i %s scanresults 2>/dev/null", (radio or "wl1")); 
	local aps = {}; 
	local obj = {}; 

	function store_object() 
		if(obj.rssi and obj.noise) then 
			-- we can only send ints on ubus so we need to return a percentage 0 to 100
			obj.snr = math.floor((1 - (obj.rssi / obj.noise)) * 100); 
		else 
			obj.snr = 0; 
		end

		table.insert(aps, obj) 
	end
	for s in stdout:gmatch("[^\r\n]+") do 
		local fields = {}
		for w in s:gmatch("%S+") do table.insert(fields, w) end
		for i,v in ipairs(fields) do
			if v == "SSID:" then 
				x = {}; 
				for w in s:gmatch("[^\"]+") do table.insert(x, w) end
				if next(obj) ~= nil then store_object(); end
				obj = {}; 
				obj["ssid"] = x[2]; 
			end
			if v == "Mode:" then obj["mode"] = fields[i+1] end
			if v == "RSSI:" then obj["rssi"] = tonumber(fields[i+1]) end	
			if v == "noise:" then obj["noise"] = tonumber(fields[i+1]) end
			if v == "Channel:" then obj["channel"] = tonumber(fields[i+1]) end
			if v == "BSSID:" then obj["bssid"] = fields[i+1] end
			if v == "multicast" and fields[i+1] == "cipher:" then obj["multicast_cipher"] = fields[i+2] end
			if v == "AKM" then obj["cipher"] = fields[i+2] end
			if v == "Chanspec:" then obj["frequency"] = fields[i+1] end
			if v == "Primary" and fields[i+1] == "channel:" then obj["primary_channel"] = tonumber(fields[i+2]) end
			if v == "WPS:" then obj["wps_version"] = fields[i+1] end
		end
	end
	if next(obj) ~= nil then 
		store_object(); 
	end
	return aps; 
end

function WifiMonitor:update_ap_list()
	local aps = self.poll_scan_results(); 
	local online_aps = {}; 
	for i,ap in ipairs(aps) do 
		online_aps[ap.ssid] = ap; 
		if self.scanresults[ap.ssid] then 
			-- the ap is already in the list
			-- we remove it from our list of previous scan results and will replace the list later
			self.scanresults[ap.ssid] = nil; 
		else 
			-- the ap is a new access point
			conn:send("juci.broadcom.wld.ap.up", ap); 
		end
	end
	-- go through remaining aps in the previous list and report them as disconnected
	for ssid,ap in pairs(self.scanresults) do
		conn:send("juci.broadcom.wld.ap.down", ap); 
	end
	self.scanresults = online_aps; 
end

local wifimon = WifiMonitor:new(); 
function start_service()
	local tscanresults; 
	local tscanstart; 
	
	function parse_results()
		wifimon:update_ap_list(); 
	end

	function do_scan()
		wifimon:scan(); 
		tscanresults:set(1000); 
		tscanstart:set(20000); 
		collectgarbage(); 
	end
	
	tscanresults = uloop.timer(parse_results); 
	tscanstart = uloop.timer(do_scan); 
end

local status = { 
	locked = false
}; 

conn:add({
	["juci.broadcom.wld"] = {
		scan = {
			function(req, msg)
				wifimon:scan(msg.device); 			
			end, { device = ubus.STRING }
		}, 
		scanresults = {
			function(req, msg)
				conn:reply(req, { list = wifimon:poll_scan_results(msg.device) }); 
			end, {}
		}
	}; 
}); 

uloop.run(); 

